<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<title>亲爱的天使生日快乐</title>
		<link rel="stylesheet" href="./style.css" />
		<script src="https://cdnjs.cloudflare.com/ajax/libs/prefixfree/1.0.7/prefixfree.min.js"></script>
	</head>
	<body>
		<!-- partial:index.partial.html -->
		<canvas id="c"></canvas>
		<!-- partial -->
<!-- 		<script src="./script.js"></script> -->
	</body>
	<style>
		canvas {
			position: absolute;
			top: 0;
			left: 0;
		}

		p {
			margin: 0 0;
			position: absolute;
			font: 16px Verdana;
			color: #eee;
			height: 25px;
			top: calc(100vh - 30px);
			text-shadow: 0 0 2px white;
		}

		p a {
			text-decoration: none;
			color: #aaa;
		}

		span {
			font-size: 11px;
		}

		p>a:first-of-type {
			font-size: 20px;
		}

		body {
			overflow: hidden;
		}
	</style>
	<script>
		var w = (c.width = window.innerWidth),
			h = (c.height = window.innerHeight),
			ctx = c.getContext("2d"),
			hw = w / 2, // half-width
			hh = h / 2,
			sentenceIndex = 0,
			opts = {
				get strings() {
					const sentences = [
						["这是你的专属礼物，请签收！", "生活是充满仪式感的！"],
						["Biu~~", "我的天使！", "下边是我对你的祝福哦！"],
						["愿你心有阳光，生活开朗；", "愿你坦荡聪慧，赤诚善良；", "愿你披荆斩棘，乘风破浪；"],
						// ["最美丽的日子，最可爱的你，","最完美的礼物，最快乐的相遇，","最精彩的做自己，是幸福的相遇，"],
						["HAPPY BIRTHDAY TO YOU!", "生日快乐！我的天使！"],
						// ["你以为结束了？","当然没有，我还有段话想对你说！"],
						// ["我们"]
						// ["我会陪你很久很久，", "是我想，也是我会，", "你懂得，只要你愿意，", "我永远都不会离开你，", "我只会把所有的温柔，", "和偏爱，都给你。"],

					];

					return sentences[sentenceIndex++ % sentences.length];
				},
				charSize: 30,
				charSpacing: 35,
				lineHeight: 40,

				cx: w / 2,
				cy: h / 2,

				fireworkPrevPoints: 10,
				fireworkBaseLineWidth: 5,
				fireworkAddedLineWidth: 8,
				fireworkSpawnTime: 200,
				fireworkBaseReachTime: 30,
				fireworkAddedReachTime: 30,
				fireworkCircleBaseSize: 20,
				fireworkCircleAddedSize: 10,
				fireworkCircleBaseTime: 30,
				fireworkCircleAddedTime: 30,
				fireworkCircleFadeBaseTime: 10,
				fireworkCircleFadeAddedTime: 5,
				fireworkBaseShards: 5,
				fireworkAddedShards: 5,
				fireworkShardPrevPoints: 3,
				fireworkShardBaseVel: 4,
				fireworkShardAddedVel: 2,
				fireworkShardBaseSize: 3,
				fireworkShardAddedSize: 3,
				gravity: 0.1,
				upFlow: -0.1,
				letterContemplatingWaitTime: 360,
				balloonSpawnTime: 20,
				balloonBaseInflateTime: 10,
				balloonAddedInflateTime: 10,
				balloonBaseSize: 20,
				balloonAddedSize: 20,
				balloonBaseVel: 0.4,
				balloonAddedVel: 0.4,
				balloonBaseRadian: -(Math.PI / 2 - 0.5),
				balloonAddedRadian: -1,
			},
			currentSentence = opts.strings;
		(calc = {
			totalWidth: opts.charSpacing *
				Math.max(currentSentence[0].length, currentSentence[1].length),
		}),
		(Tau = Math.PI * 2),
		(TauQuarter = Tau / 4),
		(letters = []);

		ctx.font = opts.charSize + "px Verdana";

		function Letter(char, x, y) {
			this.char = char;
			this.x = x;
			this.y = y;

			this.dx = -ctx.measureText(char).width / 2;
			this.dy = +opts.charSize / 2;

			this.fireworkDy = this.y - hh;

			var hue = (x / calc.totalWidth) * 360;

			this.color = "hsl(hue,80%,50%)".replace("hue", hue);
			this.lightAlphaColor = "hsla(hue,80%,light%,alp)".replace("hue", hue);
			this.lightColor = "hsl(hue,80%,light%)".replace("hue", hue);
			this.alphaColor = "hsla(hue,80%,50%,alp)".replace("hue", hue);

			this.reset();
		}
		Letter.prototype.reset = function() {
			this.phase = "firework";
			this.tick = 0;
			this.spawned = false;
			this.spawningTime = (opts.fireworkSpawnTime * Math.random()) | 0;
			this.reachTime =
				(opts.fireworkBaseReachTime + opts.fireworkAddedReachTime * Math.random()) |
				0;
			this.lineWidth =
				opts.fireworkBaseLineWidth + opts.fireworkAddedLineWidth * Math.random();
			this.prevPoints = [
				[0, hh, 0]
			];
		};
		Letter.prototype.step = function() {
			if (this.phase === "firework") {
				if (!this.spawned) {
					++this.tick;
					if (this.tick >= this.spawningTime) {
						this.tick = 0;
						this.spawned = true;
					}
				} else {
					++this.tick;

					var linearProportion = this.tick / this.reachTime,
						armonicProportion = Math.sin(linearProportion * TauQuarter),
						x = linearProportion * this.x,
						y = hh + armonicProportion * this.fireworkDy;

					if (this.prevPoints.length > opts.fireworkPrevPoints)
						this.prevPoints.shift();

					this.prevPoints.push([x, y, linearProportion * this.lineWidth]);

					var lineWidthProportion = 1 / (this.prevPoints.length - 1);

					for (var i = 1; i < this.prevPoints.length; ++i) {
						var point = this.prevPoints[i],
							point2 = this.prevPoints[i - 1];

						ctx.strokeStyle = this.alphaColor.replace(
							"alp",
							i / this.prevPoints.length
						);
						ctx.lineWidth = point[2] * lineWidthProportion * i;
						ctx.beginPath();
						ctx.moveTo(point[0], point[1]);
						ctx.lineTo(point2[0], point2[1]);
						ctx.stroke();
					}

					if (this.tick >= this.reachTime) {
						this.phase = "contemplate";

						this.circleFinalSize =
							opts.fireworkCircleBaseSize +
							opts.fireworkCircleAddedSize * Math.random();
						this.circleCompleteTime =
							(opts.fireworkCircleBaseTime +
								opts.fireworkCircleAddedTime * Math.random()) |
							0;
						this.circleCreating = true;
						this.circleFading = false;

						this.circleFadeTime =
							(opts.fireworkCircleFadeBaseTime +
								opts.fireworkCircleFadeAddedTime * Math.random()) |
							0;
						this.tick = 0;
						this.tick2 = 0;

						this.shards = [];

						var shardCount =
							(opts.fireworkBaseShards +
								opts.fireworkAddedShards * Math.random()) |
							0,
							angle = Tau / shardCount,
							cos = Math.cos(angle),
							sin = Math.sin(angle),
							x = 1,
							y = 0;

						for (var i = 0; i < shardCount; ++i) {
							var x1 = x;
							x = x * cos - y * sin;
							y = y * cos + x1 * sin;

							this.shards.push(new Shard(this.x, this.y, x, y, this.alphaColor));
						}
					}
				}
			} else if (this.phase === "contemplate") {
				++this.tick;

				if (this.circleCreating) {
					++this.tick2;
					var proportion = this.tick2 / this.circleCompleteTime,
						armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;

					ctx.beginPath();
					ctx.fillStyle = this.lightAlphaColor
						.replace("light", 50 + 50 * proportion)
						.replace("alp", proportion);
					ctx.beginPath();
					ctx.arc(this.x, this.y, armonic * this.circleFinalSize, 0, Tau);
					ctx.fill();

					if (this.tick2 > this.circleCompleteTime) {
						this.tick2 = 0;
						this.circleCreating = false;
						this.circleFading = true;
					}
				} else if (this.circleFading) {
					ctx.fillStyle = this.lightColor.replace("light", 70);
					ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

					++this.tick2;
					var proportion = this.tick2 / this.circleFadeTime,
						armonic = -Math.cos(proportion * Math.PI) / 2 + 0.5;

					ctx.beginPath();
					ctx.fillStyle = this.lightAlphaColor
						.replace("light", 100)
						.replace("alp", 1 - armonic);
					ctx.arc(this.x, this.y, this.circleFinalSize, 0, Tau);
					ctx.fill();

					if (this.tick2 >= this.circleFadeTime) this.circleFading = false;
				} else {
					ctx.fillStyle = this.lightColor.replace("light", 70);
					ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);
				}

				for (var i = 0; i < this.shards.length; ++i) {
					this.shards[i].step();

					if (!this.shards[i].alive) {
						this.shards.splice(i, 1);
						--i;
					}
				}

				if (this.tick > opts.letterContemplatingWaitTime) {
					this.phase = "balloon";

					this.tick = 0;
					this.spawning = true;
					this.spawnTime = (opts.balloonSpawnTime * Math.random()) | 0;
					this.inflating = false;
					this.inflateTime =
						(opts.balloonBaseInflateTime +
							opts.balloonAddedInflateTime * Math.random()) |
						0;
					this.size =
						(opts.balloonBaseSize + opts.balloonAddedSize * Math.random()) | 0;

					var rad =
						opts.balloonBaseRadian + opts.balloonAddedRadian * Math.random(),
						vel = opts.balloonBaseVel + opts.balloonAddedVel * Math.random();

					this.vx = Math.cos(rad) * vel;
					this.vy = Math.sin(rad) * vel;
				}
			} else if (this.phase === "balloon") {
				ctx.strokeStyle = this.lightColor.replace("light", 80);

				if (this.spawning) {
					++this.tick;
					ctx.fillStyle = this.lightColor.replace("light", 70);
					ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

					if (this.tick >= this.spawnTime) {
						this.tick = 0;
						this.spawning = false;
						this.inflating = true;
					}
				} else if (this.inflating) {
					++this.tick;

					var proportion = this.tick / this.inflateTime,
						x = (this.cx = this.x),
						y = (this.cy = this.y - this.size * proportion);

					ctx.fillStyle = this.alphaColor.replace("alp", proportion);
					ctx.beginPath();
					generateBalloonPath(x, y, this.size * proportion);
					ctx.fill();

					ctx.beginPath();
					ctx.moveTo(x, y);
					ctx.lineTo(x, this.y);
					ctx.stroke();

					ctx.fillStyle = this.lightColor.replace("light", 70);
					ctx.fillText(this.char, this.x + this.dx, this.y + this.dy);

					if (this.tick >= this.inflateTime) {
						this.tick = 0;
						this.inflating = false;
					}
				} else {
					this.cx += this.vx;
					this.cy += this.vy += opts.upFlow;

					ctx.fillStyle = this.color;
					ctx.beginPath();
					generateBalloonPath(this.cx, this.cy, this.size);
					ctx.fill();

					ctx.beginPath();
					ctx.moveTo(this.cx, this.cy);
					ctx.lineTo(this.cx, this.cy + this.size);
					ctx.stroke();

					ctx.fillStyle = this.lightColor.replace("light", 70);
					ctx.fillText(this.char, this.cx + this.dx, this.cy + this.dy + this.size);

					if (this.cy + this.size < -hh || this.cx < -hw || this.cy > hw)
						this.phase = "done";
				}
			}
		};

		function Shard(x, y, vx, vy, color) {
			var vel =
				opts.fireworkShardBaseVel + opts.fireworkShardAddedVel * Math.random();

			this.vx = vx * vel;
			this.vy = vy * vel;

			this.x = x;
			this.y = y;

			this.prevPoints = [
				[x, y]
			];
			this.color = color;

			this.alive = true;

			this.size =
				opts.fireworkShardBaseSize + opts.fireworkShardAddedSize * Math.random();
		}
		Shard.prototype.step = function() {
			this.x += this.vx;
			this.y += this.vy += opts.gravity;

			if (this.prevPoints.length > opts.fireworkShardPrevPoints)
				this.prevPoints.shift();

			this.prevPoints.push([this.x, this.y]);

			var lineWidthProportion = this.size / this.prevPoints.length;

			for (var k = 0; k < this.prevPoints.length - 1; ++k) {
				var point = this.prevPoints[k],
					point2 = this.prevPoints[k + 1];

				ctx.strokeStyle = this.color.replace("alp", k / this.prevPoints.length);
				ctx.lineWidth = k * lineWidthProportion;
				ctx.beginPath();
				ctx.moveTo(point[0], point[1]);
				ctx.lineTo(point2[0], point2[1]);
				ctx.stroke();
			}

			if (this.prevPoints[0][1] > hh) this.alive = false;
		};

		function generateBalloonPath(x, y, size) {
			ctx.moveTo(x, y);
			ctx.bezierCurveTo(
				x - size / 2,
				y - size / 2,
				x - size / 4,
				y - size,
				x,
				y - size
			);
			ctx.bezierCurveTo(x + size / 4, y - size, x + size / 2, y - size / 2, x, y);
		}

		function anim() {
			window.requestAnimationFrame(anim);

			ctx.fillStyle = "#111";
			ctx.fillRect(0, 0, w, h);

			ctx.translate(hw, hh);

			var done = true;
			for (var l = 0; l < letters.length; ++l) {
				letters[l].step();
				if (letters[l].phase !== "done") done = false;
			}

			ctx.translate(-hw, -hh);

			if (done) {
				for (var l = 0; l < letters.length; ++l) {
					letters[l].reset();
				}

				currentSentence = opts.strings;

				letters = [];
				for (var i = 0; i < currentSentence.length; ++i) {
					for (var j = 0; j < currentSentence[i].length; ++j) {
						letters.push(
							new Letter(
								currentSentence[i][j],
								j * opts.charSpacing +
								opts.charSpacing / 2 -
								(currentSentence[i].length * opts.charSize) / 2,
								i * opts.lineHeight +
								opts.lineHeight / 2 -
								(currentSentence.length * opts.lineHeight) / 2
							)
						);
					}
				}
			}
		}

		for (var i = 0; i < currentSentence.length; ++i) {
			for (var j = 0; j < currentSentence[i].length; ++j) {
				letters.push(
					new Letter(
						currentSentence[i][j],
						j * opts.charSpacing +
						opts.charSpacing / 2 -
						(currentSentence[i].length * opts.charSize) / 2,
						i * opts.lineHeight +
						opts.lineHeight / 2 -
						(currentSentence.length * opts.lineHeight) / 2
					)
				);
			}
		}

		anim();

		window.addEventListener("resize", function() {
			w = c.width = window.innerWidth;
			h = c.height = window.innerHeight;

			hw = w / 2;
			hh = h / 2;

			ctx.font = opts.charSize + "px Verdana";
		});
	</script>
</html>